namespace Radar
{
    using System;
    using System.Collections.Generic;

    public sealed class Container : IContainer
    {
        private readonly IDictionary<string, Func<ResolveContext, object>> builders = new Dictionary<string, Func<ResolveContext, object>>();
        private readonly IDictionary<string, IFactoryBuilder> factoryBuilders = new Dictionary<string, IFactoryBuilder>();

        private readonly IDictionary<Type, string> typeFullNameCache = new Dictionary<Type, string>();

        private readonly bool tryRegisterTypesAutomatically;
        private readonly Action<string, IIocConfiguration> configureCallback;

        public Container()
            : this(false, null)
        {
        }

        public Container(bool tryRegisterTypesAutomatically, Action<string, IIocConfiguration> configureCallback)
        {
            this.tryRegisterTypesAutomatically = tryRegisterTypesAutomatically;
            this.configureCallback = configureCallback;
        }

        public IIocConfiguration Register<TService, TImplementation>() where TImplementation : TService
        {
            return Register<TService, TImplementation>(GetTypeFullName(typeof(TService)));
        }

        public IIocConfiguration Register<TService, TImplementation>(string key) where TImplementation : TService
        {
            if (string.IsNullOrEmpty(key)) throw new ArgumentNullException("key");

            return RegisterInternal(typeof(TService), typeof(TImplementation), key);
        }

        public IIocConfiguration Register(Type serviceType, Type implementationType)
        {
            return Register(serviceType, implementationType, GetTypeFullName(serviceType));
        }

        public IIocConfiguration Register(Type serviceType, Type implementationType, string key)
        {
            if (serviceType == null) throw new ArgumentNullException("serviceType");
            if (implementationType == null) throw new ArgumentNullException("implementationType");

            if (!serviceType.IsAssignableFrom(implementationType))
            {
                var message = string.Format("{0} is not assignable from '{1}'", serviceType, implementationType);
                throw new IocConfigurationException(message);
            }

            return RegisterInternal(serviceType, implementationType, key);
        }

        public IIocConfiguration Register(Type serviceType, Func<object> instanceBuilder)
        {
            var name = GetTypeFullName(serviceType);

            if (factoryBuilders.ContainsKey(name))
            {
                var message = string.Format("Type '{0}' is already registered.", serviceType);
                throw new DuplicateImplementationIoCException(message);
            }

            var configuration = new IocConfiguration(this);

            factoryBuilders[name] = new ConstantFactoryBuilder(configuration, instanceBuilder);

            return configuration;
        }

        public bool IsRegistered<TService>()
        {
            return IsRegistered(typeof(TService));
        }

        public bool IsRegistered(Type serviceType)
        {
            if (serviceType == null) throw new ArgumentNullException("serviceType");

            return IsRegistered(GetTypeFullName(serviceType));
        }

        public bool IsRegistered(string key)
        {
            return factoryBuilders.ContainsKey(key);
        }

        public object Resolve(Type serviceType)
        {
            if (serviceType == null) throw new ArgumentNullException("serviceType");

            var arguments = serviceType.IsGenericType
                                ? serviceType.GetGenericArguments()
                                : Type.EmptyTypes;

            return Resolve(GetTypeFullName(serviceType), arguments, new ResolveContext());
        }

        public object Resolve(string key)
        {
            return Resolve(key, Type.EmptyTypes, new ResolveContext());
        }

        public TService Resolve<TService>()
        {
            var arguments = typeof(TService).IsGenericType
                                ? typeof(TService).GetGenericArguments()
                                : Type.EmptyTypes;

            return (TService)Resolve(GetTypeFullName(typeof(TService)), arguments, new ResolveContext());
        }

        public TService Resolve<TService>(ResolveContext context)
        {
            var arguments = typeof(TService).IsGenericType
                                ? typeof(TService).GetGenericArguments()
                                : Type.EmptyTypes;

            return (TService)Resolve(GetTypeFullName(typeof(TService)), arguments, context); 
        }

        private IIocConfiguration RegisterInternal(Type serviceType, Type implementationType, string key)
        {
            if (factoryBuilders.ContainsKey(key))
            {
                var message = string.Format("There is already an implementation of '{0}' with key '{1}'", serviceType, key);

                throw new DuplicateImplementationIoCException(message);
            }

            var configuration = new IocConfiguration(this);

            var factoryBuilder = serviceType.IsGenericTypeDefinition
                                     ? (IFactoryBuilder)new GenericFactoryBuilder(implementationType, this, configuration)
                                     : new SimpleFactoryBuilder(implementationType, this, configuration);

            factoryBuilders[key] = factoryBuilder;

            return configuration;
        }

        private object Resolve(string key, Type[] genericParameters, ResolveContext context)
        {
            if (string.IsNullOrEmpty(key)) throw new ArgumentNullException("key");
            if (context == null) throw new ArgumentNullException("context");

            context.Push(key);

            Func<ResolveContext, object> builder;
            if (!builders.TryGetValue(key, out builder))
            {
                builder = CreateBuilder(key, genericParameters);
                builders[key] = builder;
            }

            var result = builder(context);
            context.Pop(key);

            return result;
        }

        private string GetTypeFullName(Type type)
        {
            string name;
            if (!typeFullNameCache.TryGetValue(type, out name))
            {
                name = type.AssemblyQualifiedName;
                typeFullNameCache[type] = name;
            }

            return name;
        }

        private Func<ResolveContext, object> CreateBuilder(string key, Type[] genericParameters)
        {
            IFactoryBuilder factory;
            if (factoryBuilders.TryGetValue(key, out factory))
            {
                return factory.GetFactoryMethod(genericParameters);
            }

            if (tryRegisterTypesAutomatically)
            {
                var implementationType = TypeResolver.TryGetImplementation(key);
                if (implementationType != null)
                {
                    var configuration = new IocConfiguration(this);

                    var builder = new SimpleFactoryBuilder(implementationType, this, configuration);

                    factoryBuilders[key] = builder;

                    if (configureCallback != null)
                        configureCallback(key, configuration);

                    return builder.GetFactoryMethod(genericParameters);
                }
            }

            var message = string.Format("Service '{0}' is not registered.", key);
            throw new ServiceNotRegisteredException(message);
        }
    }
}